/*
 * Copyright (c) Huawei Technologies Co., Ltd. 2021-2023. All rights reserved.
 *
 */

import fse from 'fs-extra';
import path from 'path';

import { GenerateCodemodel } from '../model/cxx/generate-codemodel.js';
import { HvigorNativeConfig, NativeLibraries } from '../model/cxx/hvigor-native-build.js';
import { CmakeCache } from '../model/cxx/native-cmake-cache.js';
import { NativeCMakeFiles } from '../model/cxx/native-cmake-files.js';

import { OhosLogger } from './log/ohos-logger.js';
import { CmakeUtil } from './cmake-util.js';

const _log: OhosLogger = OhosLogger.getLogger('NativeInvalidation');

export const createInvalidationState = async (
  lastFingerPrint: FileId,
  inputFiles: FileId[],
  requiredOutputFiles: FileId[],
  optionalOutputFiles: FileId[],
  hardOutputFiles: FileId[]
) => {
  const state = InvalidationStateBuilder.newBuilder()
    .addInputFiles(inputFiles)
    .addRequiredOutputFiles(requiredOutputFiles)
    .addOptionalOutputFiles(optionalOutputFiles)
    .addHardOutputFiles(hardOutputFiles)
    .setLastFingerPrint(lastFingerPrint);

  // 解析lastFingerPrint中的所有文件信息
  const fingerPrintFiles = await readFingerPrintFile(lastFingerPrint.path);
  if (!fingerPrintFiles) {
    // 没有缓存信息, 删除CMakeCache.txt重新执行
    return state
      .setConfigureType(ConfigureType.HARD_CONFIGURE)
      .setFingerPrintFileExisted(false)
      .build();
  }

  // 计算缓存文件的修改情况
  const changeFiles = fingerPrintFiles.map(file => file.compareToCurrent());

  // lastFingerPrint未修改的文件
  const unchanged = changeFiles.filter(file => file.type === FileChangeType.UNCHANGED);

  // lastFingerPrint修改的文件
  const changed = changeFiles.filter(file => file.type !== FileChangeType.UNCHANGED);

  // 计算当前输入输出文件对比上次的变化
  const computeChanged = (files: FileId[], required = true) =>
    computeChangedFiles(changed, unchanged, files, lastFingerPrint.lastModified, required);
  const inputChanges = computeChanged(inputFiles, false);
  const outputChanges = computeChanged(requiredOutputFiles);
  const optionalChanges = computeChanged(optionalOutputFiles, false);
  const hardOutputChanges = computeChanged(hardOutputFiles);

  // soft configure input/output files
  const softChanges = inputChanges.concat(outputChanges, optionalChanges);

  const configureType =
    hardOutputChanges.length > 0 ? ConfigureType.HARD_CONFIGURE :
      softChanges.length > 0 ? ConfigureType.SOFT_CONFIGURE : ConfigureType.NO_CONFIGURE;

  // check whether any fingerprinted files were modified during this check.
  // 检查缓存文件是否在检查过程中变化
  const compareAgainChanges = fingerPrintFiles.map(file => file.compareToCurrent());
  for (const currentChange of compareAgainChanges) {
    const lastCompare = changed.find(item => item.path === currentChange.path);
    if (lastCompare && lastCompare.type !== currentChange.type) {
      _log.debug(`File ${ currentChange.path } was modified during checks for C/C++ configuration invalidation.`);
      return state
        .setFingerPrintFileExisted(true)
        .setConfigureType(configureType)
        .build();
    }
  }

  return state
    .setFingerPrintFileExisted(true)
    .addAllChangesToFingerPrintFiles(changed)
    .addAllUnchangedFingerPrintFiles(unchanged)
    .addAllSoftChanges(softChanges)
    .addAllHardChanges(hardOutputChanges)
    .setConfigureType(configureType);
};

const computeChangedFiles = (
  changed: ChangeFile[],
  unchanged: ChangeFile[],
  files: FileId[],
  lastModified: number,
  required = true) => {
  return files.map(file => {
    const change = changed.find(item => item.path === file.path);
    if (change) {
      return new ChangeFile(file.path, change.type);
    }
    if (unchanged.find(item => item.path === file.path)) {
      return new ChangeFile(file.path, FileChangeType.UNCHANGED);
    }
    if (required && !file.exist) {
      return new ChangeFile(file.path, FileChangeType.DELETED);
    }
    if (file.lastModified >= lastModified) {
      return new ChangeFile(file.path, FileChangeType.LAST_MODIFIED_CHANGED);
    }
    if (file.exist) {
      return new ChangeFile(file.path, FileChangeType.CREATED);
    }
    return new ChangeFile(file.path, FileChangeType.UNCHANGED);
  }).filter(change => change.type !== FileChangeType.UNCHANGED);
};

export const readFingerPrintFile = async (fingerPrint: string) => {
  if (!fse.existsSync(fingerPrint)) {
    return undefined;
  }
  const fingerPrintObj: ConfigureFingerPrint = await fse.readJSON(fingerPrint);
  return fingerPrintObj.files?.map<FileId>(file => Object.setPrototypeOf(file, FileId.prototype));
};

export const readCMakeFileApiReply = (dir: string, target: string, abi: string, cmakeTool: string) => {
  const generator = new GenerateCodemodel(dir);
  const cmakeFiles = generator.getObjFromIndex<NativeCMakeFiles>('cmakeFiles');
  const libraries = CmakeUtil.parseLibraries(dir, target, abi);
  let buildFiles: string[] = [];
  const extra: string[] = [];
  if (cmakeFiles) {
    const rootBuildFolder = cmakeFiles.paths.build;
    const rootSourceFolder = cmakeFiles.paths.source;
    buildFiles = cmakeFiles.inputs.map(input => path.resolve(rootSourceFolder, input.path))
      .filter(input => path.basename(input).toLowerCase() === 'cmakelists.txt' ||
        (input.toLowerCase().startsWith(rootSourceFolder.toLowerCase()) &&
          !input.toLowerCase().startsWith(rootBuildFolder.toLowerCase()) &&
            (input.toLowerCase().endsWith('.cmake.in') || input.toLowerCase().endsWith('.cmake'))))
      .sort();

    for (const library of libraries.values()) {
      library.getFiles()?.forEach(file => extra.push(path.resolve(rootSourceFolder, file)));
    }
  }

  // Populate library output
  const configLibraries: NativeLibraries = {};
  for (const library of libraries.values()) {
    configLibraries[library.id] =  {
      toolchain: 'toolchain',
      abi: library.getAbi(),
      artifactName: library.getArtifactName(),
      output: library.getOutputs()?.shift(),
      runtimeFiles: library.getRuntimeFiles()
    };
  }
  
  const config: HvigorNativeConfig = {
    buildFiles: buildFiles,
    libraries: configLibraries,
    cleanCommands: [], // command对增量判断无影响
    buildCommands: [],
    toolchain: { cCompilerExecutable: '', cppCompilerExecutable: ''},
    extra: extra
  };

  // 读取cmake cache, 获取编译器
  const cache = generator.getObjFromIndex<CmakeCache>('cache');
  if (cache) {
    const linker = cache.entries.find(entry => entry.name === 'CMAKE_LINKER');
    if (linker) {
      const extname = path.extname(linker.value);
      const parentDir = path.dirname(linker.value);
      config.toolchain.cCompilerExecutable = path.resolve(parentDir, `clang${extname}`);
      config.toolchain.cppCompilerExecutable = path.resolve(parentDir, `clang++${extname}`);
    }
  }
  return config;
};

export class InvalidationStateBuilder {
  private lastFingerPrint?: FileId;
  private inputFiles: FileId[] = [];
  private requiredOutputFiles: FileId[] = [];
  private optionalOutputFiles: FileId[] = [];
  private hardOutputFiles: FileId[] = [];
  private changesToFingerPrintFiles: ChangeFile[] = [];
  private unchangedFingerPrintFiles: ChangeFile[] = [];
  private configureType: ConfigureType = ConfigureType.SOFT_CONFIGURE;
  private softConfigureReasons: ChangeFile[] = [];
  private hardConfigureReasons: ChangeFile[] = [];
  private fingerPrintFileExisted = false;

  static newBuilder() {
    return new InvalidationStateBuilder();
  }

  async updateFingerPrint() {
    const allFiles = [
      ...this.inputFiles,
      ...this.requiredOutputFiles,
      ...this.optionalOutputFiles,
      ...this.hardOutputFiles
    ].map(file => file.update()).sort();
    if (!this.lastFingerPrint) {
      return;
    }
    if (fse.existsSync(this.lastFingerPrint.path)) {
      await fse.rm(this.lastFingerPrint.path);
    }
    await fse.writeJSON(this.lastFingerPrint.path, { files: allFiles, version: 1 } as ConfigureFingerPrint);
  }

  addAllChangesToFingerPrintFiles(changesToFingerPrintFiles: ChangeFile[]) {
    this.changesToFingerPrintFiles.push(...changesToFingerPrintFiles);
    return this;
  }

  addAllUnchangedFingerPrintFiles(unchangedFingerPrintFiles: ChangeFile[]) {
    this.unchangedFingerPrintFiles.push(...unchangedFingerPrintFiles);
    return this;
  }

  setConfigureType(configureType: ConfigureType) {
    this.configureType = configureType;
    return this;
  }

  addAllSoftChanges(softConfigureReasons: ChangeFile[]) {
    this.softConfigureReasons.push(...softConfigureReasons);
    return this;
  }

  addAllHardChanges(hardConfigureReasons: ChangeFile[]) {
    this.hardConfigureReasons.push(...hardConfigureReasons);
    return this;
  }

  setFingerPrintFileExisted(existed: boolean) {
    this.fingerPrintFileExisted = existed;
    return this;
  }

  build() {
    return this;
  }

  setLastFingerPrint(fingerPrint: FileId) {
    this.lastFingerPrint = fingerPrint;
    return this;
  }

  addInputFiles(inputFiles: FileId[]) {
    this.inputFiles = inputFiles;
    return this;
  }

  getInputFile() {
    return this.inputFiles;
  }

  addRequiredOutputFiles(inputFiles: FileId[]) {
    this.requiredOutputFiles.push(...inputFiles);
    return this;
  }

  getRequiredOutputFiles() {
    return this.requiredOutputFiles;
  }

  addOptionalOutputFiles(inputFiles: FileId[]) {
    this.optionalOutputFiles.push(...inputFiles);
    return this;
  }

  getOptionalOutputFiles() {
    return this.optionalOutputFiles;
  }

  addHardOutputFiles(inputFiles: FileId[]) {
    this.hardOutputFiles.push(...inputFiles);
    return this;
  }

  getHardOutputFiles() {
    return this.hardOutputFiles;
  }

  shouldConfigureMessages() {
    const messages: string[] = [];
    const softRegenerateMessage =
      this.configureType === ConfigureType.SOFT_CONFIGURE ? '' : ', will remove stale cache CMakeCache.txt';

    if (!this.fingerPrintFileExisted) {
      messages.push(`- no fingerprint file${softRegenerateMessage}`);
      return messages;
    }

    if (this.hardConfigureReasons.length > 0) {
      messages.push(`- a hard configure file changed${softRegenerateMessage}`);
      this.hardConfigureReasons.forEach(file => messages.push(`  - ${file.path} (${changeTypeToStr(file.type)})`));
      return messages;
    }

    if (this.softConfigureReasons.length > 0) {
      messages.push(`- a file changed${softRegenerateMessage}`);
      this.softConfigureReasons.forEach(file => messages.push(`  - ${file.path} (${changeTypeToStr(file.type)})`));
      return messages;
    }
    return messages;
  }

  shouldConfigure() {
    return this.configureType === ConfigureType.HARD_CONFIGURE || this.configureType === ConfigureType.SOFT_CONFIGURE;
  }

  softConfigureOkay() {
    return this.configureType === ConfigureType.SOFT_CONFIGURE;
  }
}

export class FileId implements FileFingerPrint {
  path: string;
  size: number;
  lastModified: number;
  exist = false;

  constructor(path: string, size?: number, lastModified?: number, exist?: boolean) {
    this.path = path;
    if (size && lastModified) {
      this.size = size;
      this.lastModified = lastModified;
      this.exist = exist === undefined ? fse.existsSync(path) : exist;
      return;
    }
    if (fse.existsSync(this.path)) {
      const stats = fse.statSync(this.path);
      this.size = size ?? stats.size;
      this.lastModified = lastModified ?? stats.mtimeMs;
      this.exist = fse.existsSync(this.path);
    } else {
      this.size = -1;
      this.lastModified = -1;
    }
  }

  static getInstance(path: string, size?: number, lastModified?: number, exist?: boolean) {
    return new FileId(path, size, lastModified, exist);
  }

  compareToCurrent() {
    const file = FileId.getInstance(this.path);
    return this.compareTo(file);
  }

  compareTo(file: FileId) {
    if (this.exist && !file.exist) return new ChangeFile(this.path, FileChangeType.DELETED);
    if (!this.exist && file.exist) return new ChangeFile(this.path, FileChangeType.CREATED);
    if (this.lastModified !== file.lastModified) return new ChangeFile(this.path, FileChangeType.LAST_MODIFIED_CHANGED);
    if (this.size !== file.size) return new ChangeFile(this.path, FileChangeType.LENGTH_CHANGED);
    return new ChangeFile(this.path, FileChangeType.UNCHANGED);
  }

  update() {
    if (!fse.existsSync(this.path)) {
      this.exist = false;
      return this;
    }
    const stats = fse.statSync(this.path);
    this.size = stats.size;
    this.lastModified = stats.mtimeMs;
    this.exist = true;
    return this;
  }
}

export class ChangeFile {
  constructor(public path: string, public type: FileChangeType) {
  }
}

export enum ConfigureType {
  UNKNOWN_CONFIGURE_TYPE,

  /**
   * No configure is needed.
   */
  NO_CONFIGURE,

  /**
   * Okay to run C/C++ configure without first deleting the configure output folder.
   */
  SOFT_CONFIGURE,

  /**
   * Configure also requires first deleting the configure output folder.
   */
  HARD_CONFIGURE,
  UNRECOGNIZED,
}

export enum FileChangeType {

  /**
   * <code>UNKNOWN = 0;</code>
   */
  UNKNOWN,

  /**
   * <code>CREATED = 1;</code>
   */
  CREATED,

  /**
   * <code>DELETED = 2;</code>
   */
  DELETED,

  /**
   * <code>LAST_MODIFIED_CHANGED = 3;</code>
   */
  LAST_MODIFIED_CHANGED,

  /**
   * <code>LENGTH_CHANGED = 4;</code>
   */
  LENGTH_CHANGED,
  UNCHANGED = -1,

}

const changeTypeToStr = (type: FileChangeType) => {
  switch (type) {
    case FileChangeType.CREATED:
      return 'CREATED';
    case FileChangeType.DELETED:
      return 'DELETED';
    case FileChangeType.LENGTH_CHANGED:
      return 'LENGTH_CHANGED';
    case FileChangeType.LAST_MODIFIED_CHANGED:
      return 'LAST_MODIFIED_CHANGED';
    case FileChangeType.UNCHANGED:
      return 'UNCHANGED';
    case FileChangeType.UNKNOWN:
      return 'UNKNOWN';
    default:
      return 'UNKNOWN';
  }
};

export interface ConfigureFingerPrint {
  files: FileFingerPrint[];
  version: number;
}

export interface FileFingerPrint {
  path: string;
  size: number;
  lastModified: number;
  exist: boolean;
}